//-----------------------------------------------------------------------
// <copyright file="Replace.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <date>2006-07-12 [04 Feb 08: Mike Fourie - updated]</date>
// <summary>Reads in a text file and modifies the contents by searching for
// and replacing a text string or text satisfying a regular expression pattern.
// </summary>
//-----------------------------------------------------------------------
namespace Microsoft.Sdc.Tasks.File
{
    using System;
    using System.IO;
    using System.Text;
    using System.Text.RegularExpressions;
    using Microsoft.Build.Framework;

    /// <summary>
    /// Reads in a text file and modifies the contents by searching for
    /// and replacing a text string or text satisfying a regular expression pattern.
    /// 
    /// Supports unicode files and case-insensitive searches.
    /// </summary>
    /// <remarks>
    /// <code><![CDATA[
    /// <File.Replace
    ///             Path="path"
    ///             Force="force"
    ///             NewValue="newValue"
    ///             TextEncoding="textEncoding"
    ///             RegularExpression="regularExpression"
    ///             OldValue="oldValue"
    ///             IgnoreCase="ignoreCase" >
    /// </File.Replace>
    /// ]]></code>
    /// <para>where:</para>
    /// <para><i>path (required)</i></para>
    /// <para>The full path and filename of the text file to be modified.</para>
    /// <para><i>force</i></para>
    /// <para>If set to true, this will modify the text file even if it is read-only. Set to false to ignore read-only files. Defaults to false.</para>
    /// <para><i>newValue (required)</i></para>
    /// <para>The new value for the specified text to search for.</para>
    /// <para><i>regularExpression (required if oldValue not specified)</i></para>
    /// <para>The regular expression pattern to match.</para>
    /// <para><i>oldValue (required if regularExpression not specified)</i></para>
    /// <para>The text string to search for.</para>
    /// <para><i>textEncoding (Optional)</i></para>
    /// <para>Default behaviour is to auto detect the encoding. Specify a valid encoding to force a particular encoding.</para>
    /// <para><i>ignoreCase (optional)</i></para>
    /// <para>If set to true, search will be case-insensitive. Set to false for case-sensitive searches. Defaults to false.</para>
    /// </remarks>
    /// <example>
    /// <code><![CDATA[
    /// <Project>
    ///     <!-- Replaces all instances of the text string "Test" in file1.txt 
    ///          with the text string "MODIFIED".
    ///     -->
    ///     <Target Name="Test" >
    ///         <File.Replace
    ///             Path="C:\SourceFiles\file1.txt"
    ///             OldValue="Test"
    ///             NewValue="MODIFIED" /> 
    ///     </Target>
    ///     <!-- Replaces runs of white space in file2.txt with a
    ///          comma and a blank.
    ///     -->
    ///     <Target Name="TestWithRegularExpression" >
    ///         <File.Replace
    ///             Path="C:\SourceFiles\file2.txt"
    ///             RegularExpression="\s+"
    ///             NewValue=", " /> 
    ///     </Target>
    /// </Project>
    /// ]]></code>    
    /// </example>
    public class Replace : TaskBase
    {
        private string path = String.Empty;
        private bool force = false;
        private string regularExpression = String.Empty;
        private string newValue = String.Empty;
        private string oldValue = String.Empty;
        private bool ignoreCase = false;
        private string textEncoding;

        /// <summary>
        /// The full path and filename of the text file to be modified.
        /// </summary>
        /// <value>The full path and filename of the text file to be modified.</value>
        [Required]
        public string Path
        {
            get { return (this.path ?? String.Empty); }
            set { this.path = value; }
        }

        /// <summary>
        /// Either the RegularExpression or OldValue attribute is required
        /// 
        /// The regular expression pattern to match.
        /// </summary>
        /// <value>The regular expression pattern to match.</value>
        public string RegularExpression
        {
            get { return (this.regularExpression ?? String.Empty); }
            set { this.regularExpression = value; }
        }

        /// <summary>
        /// Either the RegularExpression or OldValue attribute is required
        /// 
        /// The text string to match when searching.
        /// </summary>
        /// <value>The text string to match when searching.</value>
        public string OldValue
        {
            get { return (this.oldValue ?? String.Empty); }
            set { this.oldValue = value; }
        }

        /// <summary>
        /// The new replacement value.
        /// </summary>
        /// <value>The new replacement value</value>
        public string NewValue
        {
            get { return (this.newValue ?? String.Empty); }
            set { this.newValue = value; }
        }

        /// <summary>
        /// If set to true, this will modify the text file even if it is read-only.
        /// </summary>
        /// <value>If set to true, this will modify the text file even if it is read-only. Set to false to ignore read-only files. Defaults to false.</value>
        public bool Force
        {
            get { return this.force; }
            set { this.force = value; }
        }

        /// <summary>
        /// If set to true, search will be case-insensitive.
        /// </summary>
        /// <value>If set to true, search will be case-insensitive. Set to false for case-sensitive searches. Defaults to false.</value>
        public bool IgnoreCase
        {
            get { return this.ignoreCase; }
            set { this.ignoreCase = value; }
        }

        /// <summary>
        /// If set, file will be written with that encoding
        /// </summary>
        /// <value>System.Text.Encoding value. If set, file will be written in that encoding.</value>
        public string TextEncoding
        {
            get { return this.textEncoding; }
            set { this.textEncoding = value; }
        }

        /// <summary>
        /// Performs the action of this task.
        /// </summary>
        protected override void InternalExecute()
        {
            Log.LogMessageFromResources("File.RegEx", this.Path);

            // Set TextEncoding if it was specified, default to UTF-8
            Encoding encoding = Encoding.UTF8;
            if (!string.IsNullOrEmpty(this.TextEncoding))
            {
                try
                {
                    encoding = Encoding.GetEncoding(this.TextEncoding);
                }
                catch (ArgumentException)
                {
                    Log.LogError(string.Format("Error, {0} is not a supported encoding name.", this.TextEncoding));
                    return;
                }
            }

            if (System.IO.File.Exists(this.Path))
            {
                // Load contents into fileContents
                FileInfo fi = new FileInfo(this.Path);
                FileAttributes originalAttributes = fi.Attributes;

                string fileContents;
                using (StreamReader textFileReader = new StreamReader(this.Path, encoding, true))
                {
                    fileContents = textFileReader.ReadToEnd();
                    encoding = textFileReader.CurrentEncoding;
                    textFileReader.Close();
                }

                Regex regex;
                if (this.RegularExpression.Length > 0)
                {
                    // Replace matched values in fileContents using regular expression pattern.
                    if (this.IgnoreCase)
                    {
                        regex = new Regex(this.RegularExpression, RegexOptions.IgnoreCase);
                    }
                    else
                    {
                        regex = new Regex(this.RegularExpression);
                    }

                    fileContents = regex.Replace(fileContents, this.NewValue);
                }
                else if (this.OldValue.Length > 0)
                {
                    // Replace matched values in fileContents.
                    if (this.IgnoreCase)
                    {
                        // Encode text string to find, for example "abc" becomes "\a\b\c"
                        fileContents = ReplaceIgnoreCase(fileContents, this.OldValue, this.NewValue);
                    }
                    else
                    {
                        fileContents = fileContents.Replace(this.OldValue, this.NewValue);
                    }
                }
                else
                {
                    // Complain
                    throw new TaskException("File.OldValueOrRegularExpressionRequired");
                }

                // Set file attributes so that we can overwrite if necessary
                if (this.Force)
                {
                    fi.Attributes = FileAttributes.Normal;
                }

                // Write new contents to file
                using (StreamWriter textFileWriter = new StreamWriter(this.Path, false, encoding))
                {
                    textFileWriter.Write(fileContents);
                    textFileWriter.Close();
                }

                if (this.Force)
                {
                    fi.Attributes = originalAttributes;
                }
            }
            else
            {
                Log.LogError(string.Format("File not found: {0}", this.Path));
                return;
            }
        }

        /// <summary>
        /// Replaces using ignored case.
        /// </summary>
        /// <param name="searchText">The search text.</param>
        /// <param name="oldValue">The old value.</param>
        /// <param name="newValue">The new value.</param>
        /// <returns>Replaced string content</returns>
        private static string ReplaceIgnoreCase(string searchText, string oldValue, string newValue)
        {
            StringBuilder result = new StringBuilder(string.Empty, 2 * searchText.Length);
            int pos = searchText.IndexOf(oldValue, StringComparison.InvariantCultureIgnoreCase);
            while (pos != -1)
            {
                if (pos > 0)
                {
                    result.Append(searchText.Substring(0, pos));
                }

                result.Append(newValue);
                searchText = searchText.Substring(pos + oldValue.Length);
                pos = searchText.IndexOf(oldValue, StringComparison.InvariantCultureIgnoreCase);
            }

            result.Append(searchText);
            return result.ToString();
        }
    }
}